using System;
using System.Collections.Generic;
using System.Text;

namespace PDSClient.Utils.Shared
{
    public class MessageBuffer
    {
        private readonly byte[] buf; /* was final */

        private readonly int capacity; /* was final */

        private int pos = 0;

        private int limit;

        public static int GetSize(string str)
        {
            int utfLen = 0;
            for (int i = 0; i < str.Length; i++)
            {
                int c = str[i];
                if ((c >= 0x0001) && (c <= 0x007F))
                {
                    utfLen++;
                }
                else if (c > 0x07FF)
                {
                    utfLen += 3;
                }
                else
                {
                    utfLen += 2;
                }
            }
            return utfLen + 2;
        }

        public MessageBuffer(int capacity) : this(new byte[capacity])
        {
            if (capacity == 0)
                throw new ArgumentException("capacity must be greater than 0");

            this.limit = 1;
        }

        public MessageBuffer(byte[] buf)
        {
            this.buf = buf;
            this.capacity = buf.Length;
            this.limit = buf.Length;
        }

        public int Capacity { get { return capacity; } }

        public int Limit { get { return limit; } }

        public int Position { get { return pos; } }

        public void Rewind()
        {
            pos = 0;
        }

        public MessageBuffer PutByte(int b)
        {
            if (pos == capacity)
                throw new IndexOutOfRangeException();

            buf[pos++] = (byte)b;

            limit = (pos == capacity ? pos : pos + 1);
            return this;
        }

        public MessageBuffer PutByteArray(byte[] bytes)
        {
            if (pos + 2 + bytes.Length > capacity)
                throw new IndexOutOfRangeException();

            PutShort(bytes.Length);
            PutBytes(bytes);
            return this;
        }

        public MessageBuffer PutBytes(byte[] bytes)
        {
            if (pos + bytes.Length > capacity)
                throw new IndexOutOfRangeException();

            foreach (byte b in bytes)
                PutByte(b);

            return this;
        }

        public MessageBuffer PutChar(int v)
        {
            if (pos + 2 > capacity)
                throw new IndexOutOfRangeException();
            uint uv = (uint)v; /* both >> where actually >>>, fixed with the uint cast */
            PutByte((int)((uv >> 8) & 0xFF));
            PutByte((int)((uv >> 0) & 0xFF));
            return this;
        }

        public MessageBuffer PutShort(int v) 
        {
	        if (pos + 2 > capacity)
	            throw new IndexOutOfRangeException();
            uint uv = (uint)v;
	        PutByte((int)((uv >> 8) & 0xFF));	
	        PutByte((int)((uv >> 0) & 0xFF));
	        return this;
        }

        public MessageBuffer PutInt(int v)
        {
            if (pos + 4 > capacity)
                throw new ArgumentException();

            uint uv = (uint)v;
            PutByte((int)((uv >> 24) & 0xff));
            PutByte((int)((uv >> 16) & 0xff));
            PutByte((int)((uv >> 8) & 0xff));
            PutByte((int)((uv >> 0) & 0xff));
            return this;
        }

        public MessageBuffer PutLong(long v)
        {
            if (pos + 8 > capacity)
                throw new ArgumentException();

            ulong uv = (ulong)v;

            PutByte((byte)(uv >> 56));
            PutByte((byte)(uv >> 48));
            PutByte((byte)(uv >> 40));
            PutByte((byte)(uv >> 32));
            PutByte((byte)(uv >> 24));
            PutByte((byte)(uv >> 16));
            PutByte((byte)(uv >> 8));
            PutByte((byte)(uv >> 0));
            return this;
        }
        public MessageBuffer PutString(string str)
        {
            int size = GetSize(str);
            if (pos + size > capacity)
                throw new IndexOutOfRangeException();

            PutShort(size - 2);

            int strlen = str.Length;
            int i = 0;

            for (i = 0; i < strlen; i++)
            {
                char c = str[i];
                if ((c >= 0x0001) && (c <= 0x007F))
                {
                    buf[pos++] = (byte)c;
                }
                else if (c > 0x07FF)
                {
                    buf[pos++] = (byte)(0xE0 | ((c >> 12) & 0x0F));
                    buf[pos++] = (byte)(0xE0 | ((c >> 6) & 0x3F));
                    buf[pos++] = (byte)(0x80 | ((c >> 0) & 0x3F));
                }
                else
                {
                    buf[pos++] = (byte)(0xC0 | ((c >> 6) & 0x1F));
                    buf[pos++] = (byte)(0x80 | ((c >> 0) & 0x3F));
                }
            }
            // Adjust limit, because we didn't use putByte.
            limit = (pos == capacity ? pos : pos + 1);
            return this;
        }
        public byte GetByte()
        {
            if (pos == limit)
                throw new IndexOutOfRangeException();

            byte b = buf[pos++];
            return b;
        }

        public byte[] GetByteArray()
        {
            int savePos = pos;
            try
            {
                return GetBytes(GetUnsignedShort());
            }
            catch (IndexOutOfRangeException e)
            {
                pos = savePos;
                throw e;
            }
        }

        public byte[] GetBytes(int size)
        {
            if (pos + size > limit)
                throw new IndexOutOfRangeException();

            byte[] bytes = new byte[size];
            for (int i = 0; i < size; i++)
                bytes[i] = GetByte();

            return bytes;
        }

        public short GetShort()
        {
            if (pos + 2 > limit)
                throw new IndexOutOfRangeException();

            return (short)((GetByte() << 8) + (GetByte() & 255));
        }

        public int GetUnsignedShort()
        {
            if (pos + 2 > limit)
                throw new IndexOutOfRangeException();

            return ((GetByte() & 255) << 8) + ((GetByte() & 255) << 0);
        }

        public int GetInt()
        {
            if (pos + 4 > limit)
                throw new IndexOutOfRangeException();

            return ((GetByte() & 255) << 24) +
                    ((GetByte() & 255) << 16) +
                    ((GetByte() & 255) << 8) +
                    ((GetByte() & 255) << 0);
        }

        public long GetLong()
        {
            if (pos + 8 > limit)
                throw new IndexOutOfRangeException();

            return ((long)(GetByte() & 255) << 56) +
            ((long)(GetByte() & 255) << 48) +
            ((long)(GetByte() & 255) << 40) +
            ((long)(GetByte() & 255) << 32) +
            ((long)(GetByte() & 255) << 24) +
            ((long)(GetByte() & 255) << 16) +
            ((long)(GetByte() & 255) << 8) +
            ((long)(GetByte() & 255) << 0);
        }

        public char GetChar()
        {
            if (pos + 2 > limit)
                throw new IndexOutOfRangeException();

            return (char)((GetByte() << 8) + (GetByte() & 255));
        }

        public string GetString()
        {
            if (pos + 2 > limit)
                throw new IndexOutOfRangeException();

            int savePos = pos;

            /*
	         * Get length of UTF encoded string.
	         */
            int utfLen = GetUnsignedShort();
            int utfEnd = utfLen + pos;
            if (utfEnd > limit)
            {
                pos = savePos;
                throw new IndexOutOfRangeException();
            }

            // Decode string
            char[] chars = new char[utfLen * 2];
            int c, char2, char3;
            int index = 0;

            while (pos < utfEnd)
            {
                c = buf[pos] & 0xff;
                if (c > 127)
                    break;

                pos++;
                chars[index++] = (char)c;
            }

            try
            {
                while (pos < utfEnd)
                {
                    c = buf[pos] & 0xff;

                    switch (c >> 4)
                    {
                        case 0:
                        case 1:
                        case 2:
                        case 3:
                        case 4:
                        case 5:
                        case 6:
                        case 7:
                            /* 0xxxxxxx */
                            pos++;
                            chars[index++] = (char)c;
                            break;

                        case 12:
                        case 13:
                            /* 110x xxxx   10xx xxxx*/
                            pos += 2;
                            if (pos > utfEnd)
                                throw new Exception("UTFDataFormatException : malformed input: partial character at end");
                            char2 = buf[pos - 1];
                            if ((char2 & 0xC0) != 0x80)
                                throw new Exception("UTFDataFormatException : malformed input around byte " + pos);

                            chars[index++] = (char)(((c & 0x1F) << 6) | (char2 & 0x3F));
                            break;
                        case 14:
                            /* 1110 xxxx  10xx xxxx  10xx xxxx */
                            pos += 3;
                            if (pos > utfEnd)
                                throw new Exception("UTFDataFormatException : malformed input: partial character at end");

                            char2 = buf[pos - 2];
                            char3 = buf[pos - 1];
                            if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
                                throw new Exception("UTFDataFormatException : malformed input around byte " + (pos - 1));

                            chars[index++] = (char)(((c & 0x0F) << 12) |
                                ((char2 & 0x3F) << 6) |
                                ((char3 & 0x3F) << 0));
                            break;

                        default:
                            /* 10xx xxxx,  1111 xxxx */
                            throw new Exception("UTFDataFormatException : malformed input around byte " + pos);
                    }
                }
            }
            catch (Exception e)
            {
                //restore position
                pos = savePos;
                throw e;
            }
            return new string(chars, 0, index);
        }

        public byte[] GetBuffer()
        {
            return buf;
        }
    }
}
